Spring cloud Feign

需要注意的是 Feign 是除了 RestTemplate 客户端实现服务发现外 ,可以实现服务发现的另一种方式,通过整合 Ribbon 好后可以提供负载均衡的功能。

Feign 与 Ribbon 最为不同的就是调用方式,通常情况下 Ribbon 需要自己构建一个 HTTP 请求,然后 RestTemplate 将该请求发送给其他服务,RibbonClient(value = "serverName") 就印证了这一点。

而 Feign 是在 Ribbon 的基础上在进行一层封装,因为是采用接口的方式他并不需要自己构建 HTTP 请求。只需要将其他服务方法定义成抽象方法即可@FeignClient("ServerName") 注解来调用服务。

.@FeignClient 属性值应与服务中心方法名一致

Feign 工作流程

上述这些与 Feugn 的工作原理密不可分,在正常的情况下他有四个步骤,分别为:

在这个过程中,如果使用了 @FeignClient 注解,那么 Feign 客户端将会创建一个动态代理。
之后调用这个接口(即调用了 Feign 客户端所创建的动态代理),而 Feign 客户端的动态代理会根据接口上的 @RequestMapping 注解来构造出地址以及方法。
最后发起请求并解析响应

服务提供者 调用了 定义 @FeignClient 注解的接口时,Feign 会构建一个动态代理,之后构造地址
最终向接口发送请求(也就是服务提供者)

Feign 负载均衡

Feign 是一个声明式的 Web Servce 客户端,Spring cloud 客户端添加了Spring MVC 的支持,Feign 在整合了 Ribbon 后即可一共负载均衡的功能,在此之前我们需要添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

以及在 application.properties 全局配置文件中配置服务端口以及名称等:

1
2
3
4
5
spring.application.name=Feign Configuration
server.port=8780
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.register=false

然后在启动类中通过 EnableDiscoveryClient 来启用服务发现,并使用 @EnableFeignClients 注解来启动 Feign 客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
* 启动类,通过 @EnableDiscoveryClient 实现服务中心,以及 @EnableFeignClients 来支持 Feign
* @author kunlun
* @date 2021/7/1
*/
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}
Config

在配置类中我们主要用于实现使用默认契约,即 Spring cloud Netfilx MVC Controller 改为 feign.Contract.Defaull 原生契约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demo.config;

import feign.Contract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 自定义 Feign 配置
* @author kunlun
* @date 2021/7/1
*/
@Configuration
public class FeignConfig {

/**
* 将 Spring cloud MVC Netfilx Controller 改为 feign.Contract.Defaull
* 之后契约将默认改为 Feign 原生契约,之后可以使用默认注解
* @return feign.Contract.Defaull
*/
@Bean
public Contract contract() {
return new Contract.Default();
}
}

Interface

为了通过 consul 服务发现提供者,因此需要通过 FeignClient 注解来进行连接,也就是 Feign 工作流程中的 “构造地址‘,以及 Feign 的配置等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demo;

import feign.RequestLine;
import org.springframework.cloud.openfeign.FeignClient;
import com.example.demo.config.FeignConfig;

/**
* FeignClient 接口
* @author kunlun
* @date 2021/7/1
*/
@FeignClient(contextId = "feignClient", name = "service-provider", configuration = FeignConfig.class)
public interface FeignClientInterface {

/**
* 通过 @RequestLine 将 Spring MVC 注解修改为原生的 @RequestLine
* @return @RequestLine
*/
@RequestLine("GET /hey")
public String hey();

}
controller

最后的 Feign 控制器则是为了提供服务,因此只需要通过调用 Feigin 接口即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.demo.controller;

import com.example.demo.FeignClientInterface;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 调用接口以此来提供服务
* @author kunlun
* @date 2021/7/1
*/
@RestController
public class FeignController {

@Autowired
FeignClientInterface feignClientInterface;

@GetMapping("/hey")
public String index() {
return feignClientInterface.hey();
}
}

Feign 记录日志

在 Feign 中,记录日志的等级可以分为四个,分别为:

ID DA
NONE 不记录(默认)
BASIC 只记录和请求方法、URL、相应状态码以及执行时间
HEADERS 只记录基本信息,请求和响应的标题
FULL 记录请求、响应的标题以及正文和元数据

在 Feigin 负载均衡的基础上,我们实际上只需要修改或添加 config 包下的 FeignConfig 类即可:

FeignConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.demo.config;

import feign.Contract;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 自定义 Feign 配置
* @author kunlun
* @date 2021/7/1
*/
@Configuration
public class FeignConfig {

/**
* 将 Spring cloud MVC Controller 改为 feign.Contract.Defaull
* 之后契约将默认改为 Feign 原生契约,之后可以使用默认注解
* @return feign.Contract.Defaull
*/
@Bean
public Contract contract() {
return new Contract.Default();
}

/**
* 日志管理器记录等级为 FULL
* @return Logger.Level.Full
*/
@Bean
Logger.Level level() {
return Logger.Level.FULL;
}
}

Logger 是一个日志管理器,与 level 组合为 Logger.Level 即日志管理器等级

application.properties

之后在 applicatin.properties 全局配置文件中添加 logging.level.com.example.demo=DEBUG 来开启记录日志的包:

1
2
3
4
5
6
spring.application.name=Feign Configuration
server.port=8780
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.register=false
logging.level.com.example.demo=DEBUG

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Openbox

Openbox 是一种基于 X Window System 的窗口管理器,用于 Linux、FreeBSD 等类Unix系统中,由于其简洁和实用的特点被 CrunchBang(后被 bunsen lab 社区所继承)Lubuntu、ArchBang、TinyMe 等作为默认的窗口管理器。

Openbox 对用户有高度可配置且具有广泛的标准和支持,因此可以自行编写主题和修改,且在 GNOME、KDE 环境中都支持 Openbox 的使用,而对于 Openbox 的安装 Debian 系读者可以直接使用下述命令进行安装:

1
sudo apt-get install openbox

亦或者你恰巧是 Arch Linux 读者,也可以通过 pacman 来快速进行安装:

1
pacman -S openBox

除此之外还可以通过编译进行安装,有兴趣的读者可以自行查阅相关资料

如果需要从 0 开始制作一个主题,那首先是先建立一个主题的文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/usr/share/themes/Natura/
└── openbox-3
├── close_hover.xbm
├── close.xbm
├── desk_hover.xbm
├── desk_toggled.xbm
├── desk.xbm
├── iconify_hover.xbm
├── iconify.xbm
├── max_hover.xbm
├── max_toggled.xbm
├── max.xbm
├── shade_hover.xbm
├── shade.xbm
└── themerc

其中 /usr/share/themes/ 是系统范围的主题安装目录,而用户特定的主题可以安装在 ~/.local/share/themes 内。

.Natura 目录则主题名称,而往下的 penbox-3 是主题的类型,这表明他适用与 Openbox 3,但在一些非常流行的主题中,还支持:

Themes Type name
Bunsen-Blackish-Remix gtk-2.0
gtk-3.0
openbox-3
xfce-notify-4.0

随着主题目录再往下,themerc 主要主题文件,其次以 .xbm 后缀结尾的则是,如 max_hover.xbm 则代表为鼠标悬停时的最大按钮。

themerc 主题文件中,格式以 CSS 类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Window geometry
padding.width: 2
padding.height: 2
border.width: 1
window.client.padding.width: 1
window.client.padding.height: 0
window.handle.width: 3

#Menu geometry
menu.border.width: 1
menu.overlap.x: 0
menu.overlap.y: 0

# Border colors
window.active.border.color: #000000
window.inactive.border.color: #000000
menu.border.color: #000000
window.active.client.color: #ffffff
window.inactive.client.color: #cccccc

在这里都需要按照 Openbox 的规范来进行编写,如 padding.width: 2 则代表了 指定填充大小,用于分割窗口装饰中的颜色,用于给主题一个更加紧凑或宽敞的感觉(默认为横向)

不仅如此,Openbox 还支持渐变、纹理、单色的进行了详细的定义规范,除此之外还有文字的对齐、基础数据类型、贴图(Textures)等支持。


http://openbox.org/wiki/Help:Themes#padding.width

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud Ribbon 概述及流程

首先我们只需要知道在 Ribbon 和 Feign 或 RestTemplate 中,Ribbon 主要基于 HTTP和TCP出来进行客户端的负载均衡,他是由 Netfix 公司所开源项目 Netfix OSS 的一部分,可自动与 Netfix Server Discover 也就是 Eureka 进行交互。

Ribbon 是不可以独立部署的,Spring cloud Ribbon 是基于 Ribbon 的实现。基于了轮廓、随机等规则自动调用服务,当然除此之外还可以根据自身需要来定义均衡算法。

而 Feign 和 RestTemplate 都是用于实现服务发现, Feign 主要是声明试的 WebService 客户端,为我们提供了一个快捷、优雅的调用 HTTP API。后者则是需要服务的 IP地址等这些信息来实现各个服务之间的通信调用,区别就是 Feign 会 比 RestTemplate 简洁和优雅许多

负载均衡


在仔细讲解 Ribbon 与 Feign 或 RestTemplate 之前,他们都是涉及或实现出负载均衡这个功能,因此负载均衡在这里尤为重要。

负载均额(Load Balance)从字面意思上我们可以理解为当一个数据请求时可以分摊多个单元进行分流,通常负载均衡会分为服务器端负载均衡以及客户端负载均衡两种。

服务器端负载均衡

服务器端负载均衡主要是应对高并发和服务器端扩容的重要方法之一,负载均衡也通常讲的是服务器端负载均衡。服务器端的负载均衡主要通过在服务器与客户端之间添加负载均衡器进行实现,主要分为硬件和软件负载均衡,这里我们主要介绍软件的负载均衡。

无论是软件的负载均衡还是硬件的负载均衡均维护着一个正常服务清单,通过心跳机制来删除出现故障的服务节点,也可以通过他来恢复服务节点。

软件负载均衡主要是在普通的服务器上安装具有负载均衡的软件来实现请求的分发,进而实现负载均衡,需要注意的是服务器端的 “正常服务提供者清单” 是存储在负载均衡器中的。

客户端负载均衡

客户端负载均衡与服务器端负载均衡相差无几,他们的区别主要是客户端他本身拥有 “正常服务提供者清单”,在客户端负载均衡中。所有客户端管理都管理着一份自己需要访问的服务提供者清单,而这些清单大多数从服务中心进行获取。

Ribbon


Ribbon 是 Netfix 公司所开源项目 Netfix OSS 的一部分,主要提供一个客户端负载均衡的云库。他主要基于 HTTP 和 TCP 的客户端负载均衡组件,之后的 Spring cloud Ribbon 是一个基于 Ribbon 实现,主要特点是可以根据需要自定义负载均衡算法。

Choose Server 是负载均衡策略中 “线性轮询策略(Round Robin Ruie)” 工作流程内的第三步骤,其作用是获取服务列表中取服务进行请求,如果连续十次都没有获取到服务则报错。

从上图中我们可以看到,Ribbon 本身所维护着 正常服务提供者清单的有效性,如果通过 ChooseServer 不可用,则会重新从服务中心获取有效的服务提供者清单来进行更新。

Ribbon 接口类型

Type Type Info Bean Name Class Name
IClientConfig 用于读取配置,实现类是 DefaultClientConfigIcmpl,而默认值也是该类 ribbonClientConfig DefaultClientConfigIcmpl
IRule 负责处理负载均衡规则,实现类是 ZoneAvoidanceRule 默认通过他来选择实例,步骤是: ribbonRule ZoneAvoidanceRule
1.ServerList 获取所有可用的服务提供者列表
2.ServerListFileter 过滤一部分服务提供者地址
3.最后在剩下的地址中通过 IRule 选择一台服务器
IPing 用来筛选掉无法访问的实例 ribbonPing DummyPing
ServerList <server> 用于获取服务提供者地址列表,可以是一组固定地址也可是服务中心中定期查询服务提供者的地址列表 ribbonServerList ConfigurationBasedServerList
ServerListFileter <server> 在原始服务提供者地址列表中,通过使用一定的策略过滤一部分不符合条件的地址(当动态使用 ServerList时使用) ribbonServerListFilter ZonePrefernenceServerListFilter
ILoadBalancer Ribbon 入口 ribbonLoadBalancer ZoneAwareLoadBalancer or BaseLoadBalancer
ServerListUpdater 用于ServerList的更新,当服务中心的服务提供者发生变化的时候,ServerList 会根据 PollingServerListUpdater 来实现定时更新服务提供者列表 ribbonServerListUpdate PollingServerListUpdater

负载均衡器

负载均衡器很多都是通过 Ribbon 接口类型来进行实现具体的负载均衡 Bean 如:

Bean Type Class Name
LoadBalancerClient ILoad Balancer RibbonLoadBalancerClient
AbstractLoadBalancer ILoad Balancer AbstractLoadBalancer
LoadBalancerClient

在初始化时 execute() 方法会通过ILoadBalancer 来从服务中心获取服务提供者地址列表,并每 10s 来检测一下服务的可用性。如果服务端可用性发生改变,或者数量不一致,那么 RibbonLoadBanlancerClient 会从注册中心更新服务提供者地址列表,之后可以根据 IRule 来进行负载均衡。

LoadBanlancerClient 主要的职责是添加服务器、选择服务器、获取所有的服务器列表、获取可用的服务提供者列表等。

Method Name Method Info
addServers(List<Server> newServers) 向服务器初始列表中增加新的服务提供者地址列表
chooseServer(Object key) 从负载均衡器中选择一个服务器
markServerDown(Server server) 通知或标记已经暂停服务的服务器
getReachableServers() 返回获取到可用的服务提供者列表
getAllServers 获取所有看到服务提供者列表

Load Balancer Client 主要用于定义软件负载均衡的操作接口,一个典型的负载负载均衡实现需要一组服务器进行。通常一个方法来标记特定的服务器不循环,一个调用将现有的服务器列表中选择一个服务器来进行提供服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.netflix.loadbalancer;

import java.util.List;

/**
* 定义软件负载均衡的操作接口,一个典型的负载负载均衡实现需要一组服务器进行。通常一个方法来标记特定的服务器不循环
* 一个调用将现有的服务器列表中选择一个服务器来进行提供服务。
*
* @author stonse
*
*/
public interface ILoadBalancer {

/**
* 向服务器初始列表中增加新的服务提供者地址列表(host:port)
* @param 添加新服务提供者
*/
public void addServers(List<Server> newServers);

/**
* 从负载均衡器中选择一个服务器
*
* @param 负载均衡器将可用于确定返回那个服务器的对象,如为空则
* 负载均衡器将不会使用此参数
* @return 选择服务器
*/
public Server chooseServer(Object key);

/**
* 由负载均衡器的客户端掉应来通知某个服务器暂停服务,否则 ILoadBalancer 依然认为他可以提供服务
* 直到下一次检测的周提
*
* @param 标记已经暂停服务的服务器
*/
public void markServerDown(Server server);

/**
* @deprecated getServerList 在 2016-01-20 就被弃用,取而代之的则是 getReachableServers 以及 getAllServer API。
*
* @param 如果为 true 则只应用返回可用的服务器
*/
@Deprecated
public List<Server> getServerList(boolean availableOnly);

/**
* @return 返回获取到可用的服务提供者列表
*/
public List<Server> getReachableServers();

/**
* @return 获取所有看到服务提供者列表
*/
public List<Server> getAllServers();
}

AbstractLoadBalancer

Abstract Load Balancer 是 ILoad Balancer 的实现类,他包含了大多数负载均衡实现所需要的功能,典型的是 Load Balancer 的结构:

  1. 基于特定的标准可以分时段的服务提供者信息列表
  2. 通过规则的定义和实现负载均衡的策略类
  3. 定义并实现一种机制来确定服务提供者表单中的节点、可用性的类。

在这三类中,Abstract Load Balancer 类实现了服务提供者地址列表分组的作用,被 服务器组(ServerGroup) 来定义:

Method name Method info
ServerGroup
ALL:所有服务
STATUS_UP:正常运行的服务
STATUS_NOT_UP:下线或崩溃的服务
chooseServer
从负载均衡中选择一个服务器
getServerList(ServerGroup serverGroup)
获取负载均衡中所有服务的实例列表
getLoadBalancerStats()
从 Load Balancer 来获取每个服务的所有细节统计信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.netflix.loadbalancer;

import java.util.List;

/**
* AbstractLoadBalancer contains features required for most loadbalancing
* implementations.
*
* An anatomy of a typical LoadBalancer consists of 1. A List of Servers (nodes)
* that are potentially bucketed based on a specific criteria. 2. A Class that
* defines and implements a LoadBalacing Strategy via <code>IRule</code> 3. A
* Class that defines and implements a mechanism to determine the
* suitability/availability of the nodes/servers in the List.
*
*
* @author stonse
*
*/
public abstract class AbstractLoadBalancer implements ILoadBalancer {

public enum ServerGroup{
ALL,
STATUS_UP,
STATUS_NOT_UP
}

/**
* delegate to {@link #chooseServer(Object)} with parameter null.
*/
public Server chooseServer() {
return chooseServer(null);
}


/**
* List of servers that this Loadbalancer knows about
*
* @param serverGroup Servers grouped by status, e.g., {@link ServerGroup#STATUS_UP}
*/
public abstract List<Server> getServerList(ServerGroup serverGroup);

/**
* Obtain LoadBalancer related Statistics
*/
public abstract LoadBalancerStats getLoadBalancerStats();
}
BaseLoadBalancer

Base Load Balancer 类是 Abstract Load Balancer 的实现类或工具类,他可以用一个 List 集合(AllServerList)来保存所有服务实例,之后用另一个 List 保存(UpServerList)当前有效的服务实例:

1
2
3
4
5
6
@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
.synchronizedList(new ArrayList<Server>());
Method name Method info
IpingStrategy 用于定义检查服务的策略,将 ping 所有的服务器(如果执行的很慢可能是你有大量的服务器在此集群中)
chooseServer 调用 rule 类中的 choose 方法来选择服务器对象(如果没有则返回 null)
PingTask() 每 x 秒运行一次定时器任务,检查服务器列表中每个服务器/节点的状态(默认为 1000s)
markServerDown() 用于标注服务是否有效(mark Server Down called for server)
getReachableServer() 获取所有有效的服务实例列表
getAllServer() 拥有获取所有服务器实例列表
addServer() 向负载均衡器中添加一个新的服务实例列表

除此之外,Base Load Balancer 类中的子类 DynamicServerListLoadBalancer 以及 ZoneAwareLoadBalancer 作为 DynamicServerListLoadBalancer 的子类,都实现了了一些能力,分别为:

DynamicServerListLoadBalancer
在负载均衡器的基础上做了进一步的扩展,可以在服务实例清单在运行时的动态更新的实现,以及还提供了一个过滤器标准来过滤掉不符合所需标准的服务器。

Ribbon Ping 的实现

在上述介绍中,我们都设计到了 Ping 或 “心跳” 这个概念,负载均衡中 Ping 机制主要用于检测服务提供者的有效性。他会每隔一段时间执行 Ping 来判断服务器是否存活 ,而这些工作都将由 IPing 和他的实现类来负责(Ribbon 默认实现类是 DummyPing,但需要注意的是默认情况下不会激活 Ping 机制)。

Class Name Class Info
DummyPing 虚拟的 Ping 实现,当确定服务器活着的时候会返回 true
NoOpPing 设么都不做直接返回 true
PingConstant IPing 的实现类,他用于返回任何设置的内容,true 或 false(通常只要常量参数为 true 为服务实例存活,否则为失效)
DummyPing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.netflix.loadbalancer;

import com.netflix.client.config.IClientConfig;

/**
* Default simple implementation that marks the liveness of a Server
*
* @author stonse
*
*/
public class DummyPing extends AbstractLoadBalancerPing {

public DummyPing() {
}

public boolean isAlive(Server server) {
return true;
}

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
NoOpPing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.netflix.loadbalancer;

/**
* No Op Ping
* @author stonse
*
*/
public class NoOpPing implements IPing {

@Override
public boolean isAlive(Server server) {
return true;
}

}
PingConstant
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.netflix.loadbalancer;

/**
* A utility Ping Implementation that returns whatever its been set to return
* (alive or dead)
* @author stonse
*
*/
public class PingConstant implements IPing {
boolean constant = true;

public void setConstant(String constantStr) {
constant = (constantStr != null) && (constantStr.toLowerCase().equals("true"));
}

public void setConstant(boolean constant) {
this.constant = constant;
}

public boolean getConstant() {
return constant;
}

public boolean isAlive(Server server) {
return constant;
}
}

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud Consul

Spring cloud Consul 是由 HashiCorp 公司使用 Go lang 所开发的一个 服务治理 项目,主要包含了 服务治理、健康检查、Key-value 存储、多数据中心 的功能。与 其他服务中心相比,Consul 支持多数据中心,内外网服务采用不同的端口号进行监听,除此之外还可以避免单数据中心故障所导致的问题。

Consul 所使用的是 Raft(Reliable, Replicated, Redundant,d Fault An-Tolerant,可靠、可复制、可沉余、可容错) 算法来保证一致性,相比 Poxos,Raft 的目标则是提供更加清晰的逻辑分工使得算法本身可以被更好的理解。

原理流程

工作原理

服务提供者(Service Provider)在启动时向 Consul 发送一个POST 请求,来告诉自己的 IP和端口号等信息。Consul 收到了服务提供者的注册后,默认每隔10s想服务提供者发送一个健康请求,来验证该提供者是否可以正常提供服务。

Consul 中的服务提供者临时表每隔 10s 更新一次,只包含了通过健康检查的服务提供提供者。

之后服务消费者(Service Consumer)在调用服务提供者之前会从 Consul 获取存储的服务提供者 IP 和地址的临时地址表,之后发送请求给 服务提供者。

集群原理

Consul 的常见黑话有很多,如 Agent、Client、Server、DataCenter、Consensus、Gossip、LAN Gossip、WAN Gossip、RPC…… 他们的意思如下:

Agent

Consul 集群中每个成员都会运行 Agent,他是一个守护进程。主要分为 Server 与 Client 模式,能运行 DNS 或 HTTP 接口,负责在运行时检查和保持服务同步

Client

Client 转发所有 RPC 到 Server 的代理,Client 想对于是无状态的,他唯一执行后台活动是加入 LAN Gossip 池。Client 只需要较低的资源开销,以及较少的网络流量带宽等。

RPC(Remote Procedure Call)远程过程调用,该协议允许一台计算机的程序调用另一台开放网络的计算机。在 Consul 集群中主要允许 Client 请求 Server 的请求/响应机制

LAN gooip 包含了所有与位于同一个局域网或同一个数据中心的所有节点

Gossip 主要用于实现基于 UDP 的随机点到点通信

Server

具有扩展功能的代理,功能包括Raft选举、维护集群状态、响应 RPC差需、与其他数据中心交互 WAN gossip、转发查询给 Leader\远程数据中心。
他主要是在局域网内与本地客户端进行通信,通过广域网与其他数据中心进行通信,通常 Server保存集群中的配置信息(每个数据中心的 Server 数量建议为 3~5个)

Wan gooip 只包含分布在不同数据中心的所有节点,数据中心间通常采用互联网或广域网进行通信。

DataCenter

一个私有且低延迟和高宽带的网络环境

Consensus

Consensus 用于代表复制机的状态一致性,即用于表明 Server 与 领袖选举的事务顺序达成一致。

Gossip and Wan and LAN

在 Consul 数据中心中,官方给我们最好的建议数量是三台到五台之间。这是经过深思熟虑思考之后所得到的结果,当很多台数据中心加入后,则达成共识的过程很慢,但只有1~2台数据中心可用性又不高,因此所得出的权衡利弊的数量为 3~5台之间。

Gossip (第六届ACM分布式计算原理年会论文集)

Demers, Alan; Greene, Dan; Hauser, Carl; Irish, Wes; Larson, John; Shenker, Scott; Sturgis, Howard; Swinehart, Dan; Terry, Doug (1987-01-01). Epidemic Algorithms for Replicated Database Maintenance. Proceedings of the Sixth Annual ACM Symposium on Principles of Distributed Computing. PODC ‘87. New York, NY, USA: ACM. pp. 1–12. doi:10.1145/41840.41841. ISBN 978-0897912396. S2CID 1889203.

Gossip protocol 又称 Epidemic Protocol(流行病协议),该协议早在1987年发表在 ACM 论文 《Epidemic Algorithms for Replicated Database Maintenance》中,主要用于在分布式数据库系统中各个副本节点间的同步数据。

八卦通信模型 (Gossip Protocol)

无论喜欢与否,八卦在人类社会中扮演了重要的角色。Dunbar(一位人类学家)在具有争议的一本书中声称,语言的出现原因是允许闲聊,而闲聊的整个过程是不需要仔细思考和梳理的。

因此无论什么情况,毫无疑问这些流言蜚语仍然是一种在社会活动中传播非常优秀的。特别是传播速度非常狂,而这个过程对组织信息传播的企图具有一些抵抗力。

Kimmel 在书中给出了许多关于人类八卦的例子和细节

虽然八卦通常会被认为是一种传播手段,但是实际上并不仅仅是机器上的传递,而个经过处理的。具体流程为一个人收集、处理信息并将处理后的信息传递给其他人。
在这个过程中信息至少会根据其兴趣进行过滤,这样一来最有趣的新闻在传播到每个人之前就不会停止传播。

更复杂的化来讲,信息是逐渐改变的,这增加了过程的复杂性并可能导致突发行为,其中这个平台则充当了 “集体智能”的信息处理媒介。

流行病传播模型 (Epidemics Protocol)

实际上这些八卦非常类似于流行病,病毒扮演着信息的角色,而感染者则扮演者了解信息的角色。在过去的几年当中 “病毒式营销” 的概念非常火爆,通过视频分享平台以及社交网络,广告商可以有意识的利用日益高效的传播八卦广告。

而令人震惊或非常有趣的广告,特别是设计会有极大限度的提高机会,阅读者通常会通知他们的朋友等等。

分布式系统设计灵感

第一原因
八卦对于大型分布式系统来说非常有意义,主要有两个原因。其中之一就是设计新协议的灵感来源: Gossip 有几个吸引人的特性,如简单、速度、健壮等,除此还包含了缺乏中央控制的瓶颈。

这些特性对于大型分布式系统关键组成部分的 信息分发和集体信息处理(聚合) 非常重要。

第二原因
随着当今互联网的稳步发展,病毒和蠕虫的传播策略越来越为之复杂。受感染的计算机通常会组成网络(被称为 “僵尸网络”),能够进行协作和执行攻击等手法。

这对互联网基础设施构成了非常重大的威胁,为应对这些网络的一个办法就是设防防止他们的传播,这需要对流行病有很好的了解才可预防。

在本篇论文中,我们主要关注流行病以及八卦作为设计高可用组织系统和服务的灵感作为来源。

Gossip in Consul
概述

Consul 使用 Gossip 协议来管理成员资格并向集群广播消息,通过 Serf 库所提供,所基于 “SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol” 论文并做了一些必要的修改。

WAN and Lan

Consul 使用了两个不同的八卦池,分别称之为 Lan 与 Wan 池,每个数据中心都有一个 Lan 池,其中包含数据中心的所有成员(包括客户端和服务器)

Lan 池的主要作用是成员信息允许客户端自动发现服务器,以此来减少所需的配置量。在分布式故障检测中允许检测的工作由整个集群共享,而不是单个服务器进行, 他包含了所有位于同一个局域网或者同一个数据中心的所有节点

而 Wan 池是全局唯一的,无论数据中心如何,所有服务器都应参与WAN池。WAN 池提供成员信息允许服务器执行夸数据中心的请求。

故障检测允许 Consul 优雅的处理丢失连接的整个数据中心,或仅处理远程数据中心的单个服务器,他只包含了分布在不同数据中心的所有节点

集群实现原理

Leader and Client&Server

从上图可以得知,每个数据中心的 Client 以及 Server 是混合的,一般官方建议数据中心 Server 通常为 3~5 台。

这是经过深思熟虑之后所得出的结果,因为如果有太多的机器加入则达成共识会变慢。但只有 1~2台的数据中心可用性又不高,于是 3~5台是最好的选择

数据中心的领袖(Leader)是有一个额外工作的 Server,他是由所有 Server 选出的(Raft 协议),主要用于处理所有的查询和事务。由于需要遵一致性协议的要求,所以事物也必须被复制到其他所有节点中(CAP 协议)。

每个数据中心的 Server 都是 Raft 节点集合的一部分,在这期间内如果有一个非领袖服务器收到了RPC请求,将会将请求转发给集群中的领袖服务器中。

WAN and LAN

为了允许数据中心能够以 “低耦合(Low-touch)”的方式发现彼此。那么通过 WAN gossip Pool 来只包含服务节点的信息,用于优化网络延迟,那么一个新的数据中心就很容易加入到现存的 WAN gooip 池中。

无论数据中心如何,所有服务器都应参与WAN池的原因,所以服务也支持跨数据中心请求

这个时候一个服务在收到了来自另一个数据中心的请求后,会随即将该请求转发给正确的数据中心的服务中。之后该服务再将请求转发给本地的领袖服务(Leader),这使得服务之间有一个很低的耦合。

由于 Consul 自身的一些优势,如提所提供的健康检查、链接缓存、以及复用等功能,使得跨数据中心请求都是相对快速且可靠的。

Consul 集群实现

install

1
2
3
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install consul

至于第二条命令可能很多 debian er 无法使用,我们需要安装 sudo apt-get install software-properties-common 软件包来解决你 add-repository 无法使用的情况

当你执行第三条命令的时候,也就是 update 对于一些非常纯净的发行版,你可能还需要安装 sudo apt-get install apt-transport-https

当安装完成后我们可以执行 consul 来验证是否安装成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# consul 
Usage: consul [--version] [--help] <command> [<args>]

Available commands are:
agent Runs a Consul agent
catalog Interact with the catalog
event Fire a new event
exec Executes a command on Consul nodes
force-leave Forces a member of the cluster to enter the "left" state
info Provides debugging information for operators.
join Tell Consul agent to join cluster
keygen Generates a new encryption key
keyring Manages gossip layer encryption keys
kv Interact with the key-value store
leave Gracefully leaves the Consul cluster and shuts down
lock Execute a command holding a lock
maint Controls node or service maintenance mode
members Lists the members of a Consul cluster
monitor Stream logs from a Consul agent
operator Provides cluster-level tools for Consul operators
reload Triggers the agent to reload configuration files
rtt Estimates network round trip time between nodes
snapshot Saves, restores and inspects snapshots of Consul server state
validate Validate config files/directories
version Prints the Consul version
watch Watch for changes in Consul

run

可以通过以开发者模式运行和服务器模式运行两种,可通过使用下述命令直接进行启动:

1
2
consul agent -dev
# consul agent -server

.-dev or -server 的区别主要是开发者模式和服务器模式两种运行模式,而 agent 则是用于注册服务、运行健康检查的一个功能。每个数据中心都要求至少有一台 Agent (Server 模式) ,推荐于 3~5 台。

他主要有 Client 模式和 Server 两种模式,其中 Client 模式主要是用于运行写上述的服务(注册服务、健康检查……)而 Server 则是主要接收这些功能查询后的数据。

当运行之后我们可以通过访问 http://127.0.0.1:8500/ 来进行查看和管理 Consul 集群状态

常用命令

ID DA FA
members 列出 Consul 下集群成员
-datailed 查看 Consul 集群成员详细信息
monitor 持续打印当前 Consul 代理流日志
agent 运行 Consul 代理
-dev 开发者模式
-server 服务器模式
join 加入某个集群

vagrant init

Consul 集群的实现也非常简单,但多出了 join 参数,主要是加入集群。而在测试环境中,在正常且资金不允许的情况下我们是不会上服务器的,所以上也有很多文章出现了一些迷惑性文章。

比如,运行 xxx 命令,之后执行 join,这就很无语。首先,我们只要运行一个单个 consul 集群就会发现,他 并不是交互式的,你这个终端就挂着这个,之后你在开个新终端,来加入他,这也合理。

不要问为什么是 Vagrant 问就是 HashiCorp 项目下的(与 Consul 一个公司出的)

但是最不合理的情况发现了,就是 -bind 参数下到底要填写什么。通常情况下我们会通过 vagrant 来部署测试环境进行实现。

但幸运的是我们不需要单个手动配置 Vagrant 环境,只需要下载 Consul 所为我们提供的集群测试环境即可(实际上就是两个 debian 装个 Consul):https://github.com/hashicorp/consul/blob/master/demo/vagrant-cluster/Vagrantfile

我们可以选择手动安装或自动安装,即执行 vagrant up 根据 Vagrantfile 文件内配置执行一些命令。如 Consul 所提供的则是自动安装 Consul,则可能会造成卡住的问题,读者可自行进行解决。

当配置好测试环境后,我们即拥有了三台测试环境,分别为 vagrant box#2 以及自己本机的环境,我们要保证这三台环境内可以正常启动 consul 以及其他等等,之后即可进入下一步。

localhost

我们首先需要将我们本机成为整个集群中的 领导者(server/leader),之后剩下的 n1、n2 则扮演客户端的角色进行:

1
consul agent -dev -node=consul-server -bind=172.20.20.1 -data-dir=data -client 0.0.0.0

通过上述命令,我们主要开启了 测试 模式下的 consul ui ,可通过访问 http://localhost:8500/ui 来直接进行通过可视化的方式观察集群状态。

.-bind 参数为本机器的 IP 地址。

在实际的生产环境中,我们时候使用的则是 consul agent -server -bootstrap-expect=2 -node=consul-server -bind=172.20.20.1 -data-dir=data -client 0.0.0.0 来通过 bootstrap-expect 限制集群数量等。

而生产环境是不需要可视化 ui的,因此我们可以通过下面几个 api 接口来进行查看:

ID DA
ip:port/v1/status/leader 显示当前集群的领袖服务器
ip:port/v1/agent/members 显示集群中所有成员信息
ip:port/v1/status/peers 显示当前集群中的 Server 成员
ip:port/v1/catalog/services 显示所有服务
ip:port/v1/catalog/nodes 显示集群节点的详细信息

vagrant up

n1

在 n1 中,我们可以通过使用 vagrant ssh n1 直接进入 n1 的 debian 环境,之后建立一个目录也可以之际运行:

需要注意的是当 vagrant init 完事之后,才可以使用 vagrant up 来进行启动,最后通过 vagrant shell n1 来选择进入多个环境下进行工作。

1
consul agent -node=client-one -bind=172.20.20.10 -enable-script-checks=true -data-dir=data

通过使用 enable-script-checks 来开启 consul 的健康检查,我们也可以在开一个 n1 交互式 shell 进行加入服务集群:

1
consul join 172.20.20.1
n2

n2 的流程和步骤也基本上和 n1 相差无几,我们也是通过 vagrant ssh n2 进入环境,之后根据自己喜好决定是否建立新的目录(主要存储文件):

1
consul agent -node=client-two -bind=172.20.20.11 -enable-script-checks=true -data-dir=data

之后你可以在建立一个 shell n2 连接来加入 consul-server 集群中:

1
consul join 172.20.20.1

服务提供者以及服务消费者

通过 Consul 实现服务提供者和服务消费者之前,我们首先需要在构建项目中选择 Spring web、Spring Boot Actuator、Consul Discovery 等项目的依赖,当然也可以在 pom.xml 文件下进行添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

之后我们的服务消费者不需要 spring-boot-starter-actuator 依赖。

服务提供者


我们首先模仿下真实环境下的三台服务器,分别运行着功能服务,因此我们主要通过 .properties 进行实现和模拟这个环境。在此之前,我们首先需要对启动类中添加 Consul 的服务注册注解 @EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
* DemoApplication.class
* @author kunlun
* @data 2021/6/19
*/
@SpringBootApplication
@EnableDiscoveryClient
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

properties

application.properties
1
2
3
4
5
6
spring.application.name=consul-provider
server.port=8501
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=service-provider
provider.name=provider
logging.level.ROOT=info

需要注意的是 spring cloud consul discovery service-name 是一个 spring cloud consul 发现服务时的名称,而 logging.level.ROOT 则是用于描述日志等级的。

application-consul-provider-one.properties
1
2
3
4
5
6
spring.application.name=consul-provider-one
server.port=8502
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=service-provider
provider.name=provider-one
application-consul-provider-two.properties
1
2
3
4
5
6
spring.application.name=consul-provider-two
server.port=8503
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=service-provider
provider.name=provider-two

Controller

HeyController.java

当配置完启动类和全局文件之后,我们首先需要实现服务提供的接口信息,所提供的也非常简单,就是 .properties 文件的内容获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* HeyController.class
* @author kunlun
* @date 2021/06/19
*/
@RestController
public class HeyController {
@Value("${provider.name}")
private String name;

@Value("${server.port}")
private String port;

@RequestMapping("/hey")
public String hey() {
String string = "提供者名称:" + name + " 端口号为:" + port;
return string;
}
}

run

1
2
3
$ java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=consul-provider-one
$ java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=consul-provider-two
$ java -jar demo-0.0.1-SNAPSHOT.jar

读者可能非常疑惑 spring.profiles.active 的作用是什么,实际上 profilespro(perties 的缩写,与 file 相加所等于的 profiles。可以理解为当多个 properties 文件时,运行那个配置文件,之后访问 ip:8501/hey、8502/hey、8503/hey 都可以看到返回的信息。

服务消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.properties

1
2
3
4
5
6
spring.application.name=consul-consumer
server.port=8504
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.register=false

因为服务消费者并不需要提供服务,所以我们可以选择通过 spring.cloud.consul.discovery.register=false,让其不注册服务到 consul 集群中

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

/**
* HeyController.java
* @author kunlun
* @date 2021/06/19
*/
@RestController
public class HeyController {

@Autowired()
private LoadBalancerClient loadBalancerClient;

@GetMapping("/hey")
public String hey() {
ServiceInstance serviceInstance = loadBalancerClient.choose("service-provider");
URI uri = serviceInstance.getUri();
String callService = new RestTemplate().getForObject(uri + "/hey", String.class);
return callService;
}
}

在这其中起到主要作用的就是 RestTemplate,他起到了服务提供者负载均衡的功能。假设我们有三台服务提供者,那么我们每次刷新服务消费者时,会出现不同的服务名称及端口号等信息。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud Raft and CAP

拜占庭容错算法(Byzantine Generals Problem)

理想流程

拜占庭容错算法也叫拜占庭将军问题(Byzantine Generals Problem),由莱斯利-兰波特在论文中所提出的分布式对等网络通信容错问题。

假设一组罗马(拜占庭)将军各率领一直军队 共同 围攻一座城市,为简化问题,各种军队的行动策略分别为 进攻与撤退 两种。

每个支部队的将军必须通过投票来达成一致策略(所有军队一起进攻或撤退),因为没支部队位置不同,所以需要 信使 来进行没支部队的互相联系。

此时每支部队将军可以根据自己的投票和其他将军投票的信息来共同知道所决定的行动策略。

实际问题

一致性

假设在这些8个将军中出现了1叛徒,他们不仅可能向较为糟糕的策略进行投票,还可能选择性的传递投票信息(假设4位将军投的进攻,4位将军投的撤退,那么给进攻的将军说他们投的都是进攻,给投撤退的将军说投的都是撤退)。

那么这样各支军队的一直协调即一致性遭到了破坏

可靠性

由于各个将军需要通过 信使进行通讯,那么叛徒将军可能通过伪造的信件以及以其他将军的身份发送假的投票。

即使我们保证了所有将军都可靠的情况下,也不能排除信使被敌人截杀或叛变以及替换等情况,因此很闹保证人员的 可靠性。:

拜占庭容错

假设整套体系都按照理想化正常情况下,将军们仍然可以通过 多数决定战略,而这将会被称之为 拜占庭容错

Raft(Reliable,Replicated,Redundant,And Fault-Tolerant)

概述

Raft 集群(Reaf cluster)中主要分为 领袖(leader)、追随者(follower)、候选人(candidate)

在一个 Raft 集群中,正常情况下都会有一个领袖,其他都是追随者,领袖会负责所有的外部请求(如果不是领袖机器收到,则请求将会被导到领袖中)。

领袖会有一个固定时间发送消息,即 “心跳(heartbeat)”,让追随者知道领袖还在正常运作。每个追随者都会设计一个计时机制(timeout),当超过一定(通常在 150ms、300ms)时间没有收到领袖的心跳,那么集群将会进入到选举状态。

领导选举

假设领袖机器死机,那么就需要选择新的领袖。此时进入一个新的任期开始选举,成功当选的领袖开始工作,知道他死了出现故障为止,此时将开启新的一轮选举。

选举是 候选人 所发动的,当领导心跳超时的时候追随者就会将自己的任期编号+1,来表示自己参加竞选并投自己一票,并向其他服务器拉票(每台服务器任期只会投一票,固定给最早拉票追随者

如果候选人收到了其他候选人的拉票,且拉票的任期编号大于自己的任期编号,就会人定位落选并成为追随者,以此认为来拉票的候选人为领袖,如果有候选人收到了过半的宣判就当选为新的领袖

假设在 REST 服务器超时期限过了还没有选出新的领袖时,那么任期将会自动终止,开启新的一场选举。

Raft 的每台服务器超时期限都是随机的,这也降低了同时竞选的几率,也降低了两个竞选人得票不过半的选举失败几率。

这场选举也许不是最为公平的,因为当领袖死机的时候,每个已存储指令必定在服务器中已经写入过半。而选举流程则是会让记录较为完整的候选人来胜选,因为在候选人拉票的时候会透漏自己记录的最新信息。

记录复写


记录复写主要的主角在领袖身上,在 Raft 集群中有一个复写的状态机(State machine)来执行外来的指令,而领袖接受指令来写入记录中。

之后将指令转发给追随者,如果追随者没有反应,领袖会不断的重新发送指令给每个追随者,直到每个追随者将新指令写入记录中为止。

最后领袖收到过半追随者确认写入的信息后,将会将指令视为已存储(committed),追随者发现状态变成已存储,将会在状态机上执行该命令。

当领袖死机的时候,领袖的某些新指令还没有到写入集群当中,因此会造成集群记录处于不一致的问题,因此为解决此问题:

新领袖会担有重返一致性的责任,让每个追随者记录都和他一致。他将每个追随者记录进行比较,找出两者一致的最后一个不一致的指令进行删除,将自己之后的指令拷贝个追随者。
假设追随者司机,那么给他转发的所有指令都会被回应失败,而发送端会持续重新发送。当这台追随者重新加入集群时,就会收到这些指令并重新回应(已经写入的指令不会被重新写入)。

布鲁尔定理(Brewer’s theorem)

概述

布鲁尔定理(Brewer’s theorem)也被称之为CAP定理(CAP theorem),主要指出咋分布式系统中的:

  1. 一致性(Consistency),执行同一个举动
  2. 可用性(Availability),每次请求都能获取到正确的响应
  3. 分区容错性(Partition tolerance),保证服务宕机时其他服务依然可以正常提供服务(系统中任意信息丢失或失败不会影响系统的继续运作)。

三个场景

通过 CAP 定理,我们无法同时满足一致性、可用性、分区容错性这三个特性,因此我们假设:

CP ( 一致性和分区容错性)

CP 即 一致性、分区容错性,牺牲掉了可用性保证了一致性,可能会有几个节点不可用,通常适用与银行系统。

AP(可用性和分区容错性)

AP 是可用性和分区容错性,舍弃掉了一致性,保障服务可用但可能会造成数据的冲突,可适用流量访问大对系统正常提供服务要求较高的系统;

CA(一致性和可用性)

一旦集群中一台服务无法正常提供服务则会造成当前集群完全崩溃,适用与挑战者。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Less 命名空间

Less 命名空间无非就是遵守目前最为流行和通用的 BEM 命名规范,他有三种符号来表示扩展关系,主要代表了 BEM 中的 块(Block)、元素(Element)、修饰符(Modifier)

ID DA FA
- 中分号为连接字符使用,表示某块或子元素的多单词链接符号 xxx_jiu-xiang-zhe-yang
__ 双下划线用于表示块的子元素 list__title{}
_ 单下划线描述一个块或块子元素的状态 list_title_active

BEM

块 (Block)

块(Block),我们如果想更简单的理解的话,他其实就是这一范围的统称。就比如一个列表,那么他的块就是 .list

1
2
.list {
}

元素(Element)

从上往下,块的下面可以添加元素,假设我们需要为列表增加一个标题样式,就是 **元素 (Element)**,那么可以这样写:

1
2
3
4
5
6
.list {
&__title {
color: #007fff;
font-weight: bold;
}
}

而 View 可以直接也根据这个层级顺序进行使用样式:

1
2
3
<div class="list">
<p class="list__title">hello</p>
</div>

修饰符(Modifier)

修饰符我们可以理解为就是一个元素的状态,假设一个标题有:大、中、小 三个大小,为了让他更加的明显,我们可以使用修饰符进行区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.list {
&__title_big {
font-size: 10em;
font-weight: bold;
color: #007fff;
}
&__title_middle {
font-size: 5em;
font-weight: bold;
color: #007fff;
}
&__title_small {
font-size: 1em;
font-width: bold;
color: #007fff;
}
}

而 view 识图依然可以根据层级关系进行引入

1
2
3
4
5
<div class="list">
<p class="list__title_big">hello</p>
<p class="list__title_middle">hello</p>
<p class="list__title_small">hello</p>
</div>

BEM 的书写风格并不于此,只需要遵守其 层级关系即可。

File name

当然除了类名的书写规范,还有文件的名称,我们以腾讯的 Less 书写规范为例:

ID FA
lib-base.less 用于预定义变量:“颜色、字号、字体……”
lib-mixins.less 用于混合的代码
lib.reset.less 页面的初始布局
lib-ui.css 颗粒化 ui 样式
xxx.less 模块样式

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud 应用程序分层与响应式

MVC(Model-View-Controller)


MVC(Model View Controller)是在软件工程中较为常见的一种架构模式,无论是 Spring cloud 以及 Spring、Spring boot 基本上我们都介绍过此模型。

如果要简单的理解 MVC 的话,我们可以从上图了解到,首先 Model 是拥有对数据直接访问的权利(例如数据库访问)。

Conttroller 则是可以控制应用程序的流程,并对事件作出响应,这也包括了对 Model 的操作。

最后则是 View ,他能够显示资料,通常为前端应用。在上图中 Controller 操纵 Model 进行数据的更新或插入,之后交给 View 进行显示给用户。

如果说前面我们介绍的都是 MVC 概述,那么通过上图我们可以详细的进行理解。首先浏览器一看是看到的并不是 View,而是 通过 DispathcherServlet 来调用视图渲染后返回给浏览器中。这里我们需要注意的是 DispathcheServlet 继承于 HttpServlet ,主要负责协调和组织不同组件来完成请求并返回响应的工作,他的流程如下:

  1. 浏览器发送请求,服务器接受请求并转交给 DispatcherServlet 进行处理
  2. DispatcherServlet 匹配在控制器中配置的映射路径,进行下一步处理
  3. View 请求将 Model 和 View 解析成 View,之后调用 render() 方法根据 Model 和 View 中的数据渲染出页面。

三层架构

这与 MVC 的三层架构有很大的关系,所谓三层架构即 表现层(UI)、业务逻辑层(Service)、数据访问层(Dao\Repository)三种,他们分别表示:

Responsitory 是存取和管理对象,而 Dao 则是存取对象

  1. 表现层:用于展示界面,接收用户请求并返回数据,为客户端提供应用程序的访问接口(View)
  2. 业务逻辑层:也称之为服务层,负责业务逻辑的处理,主要调用D哦曾对数据进行 CURD 的操作
  3. 数据访问层:与数据库进行交互的持久层,被 Service 调用,在 Spring Data JPA 中则由 Hibernate 进行实现。

WebFlux

反应式是关于异步和事件驱动的非堵塞应用程序,并且需要少量线程进行垂直扩展,而不是水平扩展(集群)。反应式系统具有某些特征,可以使其成为低延迟、高吞度量工作的理想选择。

WebFlux 是在 Spring Framework 5.0(5.0.0.M5)所引入的一种反应式 Web 框架。WebFlux 可以在资源有限的情况下提高系统的吞度量和垂直扩展,这意味着在同样情况下 WebFlux 的吞吐量明显优于 MVC。

响应式(反应式)与命令式

为了应对高并发环境下服务端开发,微软提出了一个实现异步编程方案,即 反应式编程(Reactive Programming)。其他技术和社区为了跟上脚步,因此向 Netifix 等公司都也提供了类似的技术,这使得 Java 平台也有了能够实现反应式编程的框架

响应式编程或反应式编程(Reactive programming)是一个面向数据流和 变化传播 的声明式编程范式(Declarative programming)。

响应式编程与命令式编程对立,他的目标是让计算机明白,而非流程,声明式编程不需要告诉电脑问题,即告诉结果,让机器自行解决

而响应式编程则是是意味着式子 a=b+c,这意味着 a 是由 b 和 c 计算而出,如果后续有变化会影响到 a 的值,这也是 即告诉结果,让机器自行解决

命令式编程或指令式编程(Inperative programming),详细命令机器去处理一种事情而达到你想要的结果
变化传递我们可以理解为在命令式变成中,假设式子为 a=b+c 那么 a 的值就来自 b和c计算出的,如果后续有变化不会影响到a的值,这也是为什么他是 细命令机器去处理一种事情而达到你想要的结果

Spring WebFlux 是一个从头开始构建的非堵塞Web框架,可利用多核下一代处理器并处理大量并发。而 WebFlux 与 Spring MVC 数据流的选择也各不相同,前者是 Reactive Streames,后者则是 Servlet API


自然而然,既然 WebFlux 是用的是响应式数据流,而Spring MVC 选择的是 命令式数据流,自然而然 WebFlux 会更快。我认为上图很好的诠释了这一点,Spring MVC 是一个 响应 -> 准备数据 -> 返回数据 的一个过程。而 WebFlux 则是 响应 -> 返回数据 -> 开启一个新的 Work 线程进行准备 -> Work 线程完成工作 -> 返回结果 从而让对方觉得 WebFlux 会更快的一点。

Reactor

Peactor 框架由 Pivotal 基于 反应式编程(Reactive Programming) 思想进行实现,符合反应式编程的规范。

响应式编程

响应式编程(Reactive Programming)是一种非堵塞且事件驱动数据的开发方案,使用函数式编程的概念来操作数据流。

函数式编程是一种编程方式,将计算机的运算视为函数计算,如何直接和生动形象的表现出函数式编程的特点呢,那么下面的表达式就非常的清晰

过程式编程: var a = 1 + 2;
函数式编程: var result = subtract(multiply(add(1,2), 3), 4);

其实函数式变成并没有什么特殊,他其实就是将一数复制给变量或者一个数组中,就这个意思。
函数变成最主要的是λ演算 (Iambda calculus),而λ演算可以 接受函数党组输出(参数)和输出(返回值),相比过程化编程相比,函数式编程里的函数可以随时调用

事件 方法
包含元素信息 onNext()
序列结束消息 onComplete()
序列出错消息 onError()

这是通知与订阅者对应的方法,但如果我们继续眼神可以了解到背压机制(与本文有关):

事件 Iterable 迭代模式 (拉\pull) Observable 观察模式(推\push)
获取数据 T next() onNext()
处理完成 hasNext() onCompleted()
发现异常 throws Exception onError()

1.事件发布者(Publisher) 主动推送数据给 订阅者(Subscriber),出发 onNext() 方法;
2.事件发布者发生异常,则触发 订阅者 的 onError() 方法进行异常捕获处理
3.事件发布者每次推送都触发一次 onNext() 方法,当所有推送无异常时, onCompleted() 方法在在最后触发一次。

我们可以从被订阅者和订阅者理解为服务提供者和服务消费者。

如果 被订阅者发布的消息太快了,超过了订阅者的处理速度,那么这个时候就需要通过 背压机制(Backpressure),使得订阅者能够控制消费消息的速度。

实际上除了 迭代模式 (lterable) 其他的都是我们 响应式编程API的使用方式,实际上激素和观察者模式(Observable)的扩展。

Reactive Streams

他由 Netifix,TypeSafe、Pivatol共同制定,这是 Java 平台上 RxJava、Scala、Akka、Spring、Reactor 等项目的主要维护者所共同发起的。

反应式流(Reactive Streams)是一项倡议,主要用于反应式编程相关的规范以及接口提供标准。

他主要有四个组件构成,分别为发布者、订阅者、订阅、处理器等。除此还有三个接口最为主要:

  1. Publisher(事件发布者)
  2. Subscriber(订阅者)
  3. Subcription(订阅)

Mono 与 Flux

Mono 与 Flux 都是事件的发布者,也是 Reactor 的核心类:

  • Mono
    • 实现了 org.reactivestreams.Publisher 接口,用于返回单个数据
  • Flux
    • 同样实现了 org.reactivestreams.Publisher 接口,用于返回多个数据。

如果根据 ID 查询某个 User 对象,那么返回的肯定是单个数据,那么就需要使用 Mono<User>

但是如果要获取所有 User,则返回的是多个数据,需要使用 Flux<User>

需要注意的是 Mono 与 Flux 之间可以 互相转换,对一个 Flux 序列进行技术操作,得到的结果是一个 Mono<Long> 对象,将多个 Mono 序列合并在一起,则得到的是 Flux 对象。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud 云原生概述

目前我们所谈论的云原生大多数分为两个,一个由 spring 开发公司 Pivotal 所定义的,另一个则是由谷歌所牵头成立的 云原生计算基金会 (CNCF,Cloud Native Computing Foundation)。

本文我们仅理解 Pivotal 公司和部分 CNCF 所定义的为服务,其主要概述主要分为四类,分别为:

  1. 微服务
  2. DevOps
    3. CI\CD(持续集成和持续交付
  3. 容器化

微服务

读者可通过下一篇文章深度和实践中了解到微服务到底可以做什么,他的作用都会被理解,主要明确服务之间的解藕,多个服务集群。

DevOps

DevOps(Development Opertainos)组合,即开发和运维组合,可以减少应用程序发布的风险。

CI\CD(Continuous integration \ Continuous deployment),及持续集成和持续交付的所略词,其主要概念是通过应用程序的共建、测试和部署中实施自动化,在开发和运营团队架起桥梁,也是构成 DevOps 关键之一。

容器化

容器化即以容器为基础,在容器中运行的进程和应用,其 Docker 是应用最为广泛的容器引擎。通过容器封装让进程和应用成为一个独立的单元,并实现除了高水平的资源隔离,简化了维护。

除此之外还有 kubernetes 容器编排系统,主要用于容器管理和容器之间的负载均衡,都是实现容器话的关键。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud 跨服务查询与微服务部署概述

跨服务之间查询

在微服务中,如果需要服务之间与服务之间进行数据的查询,可以被称之为 “跨服务”,而进行这个过程的有两种,分别为 API 和 CQRS模式两种。

API 组合器模式


API 组合器模式由两个或多个服务提供者组成,服务提供者拥有数据服务(即数据库),API 组合器通过查询每个服务提供者的 API 结果并组合,来实现从多个服务检索数据库的查询。

CQRS


命令查询职责分离(CQRS,Command Query Responsibility Segregation),是由 Eiffel 语言之父 Betrand Meyer 提出的概念,简单来讲就是将业务上分离了 读(query)和写(command)的行为,就比人的睡和醒是两种行为。

  1. 命令(Command),不返回任何结果(void),但会改变对象状态,指增加、删除、修改
  2. 查询(Query)用于返回结果(void),不会改变对象状态

因此 CQRS 中主要强调 读(Query)和写(Command)的分离 ,可能当查询数据会有一些问题,如时间过时的数据问题,可以通过使用事件(Event)机制来解决问题并修改查询数据源。

微服务部署

部署模式


微服务的部署主要可以分为四种,这四周部署模式中主要通过下述方式进行:

  1. WAR、JAR 包运行
  2. 服务部署虚拟机运行
  3. 通过 Docker 或 Kubernetes 等容器化技术进行微服务部署
  4. Serverless 无服务器部署

无服务器(Serverless)通过应用 使用第三方功能或服务,不需要管理服务器,但不代表他不需要服务器,主要包括了:

功能即服务(Function as a Service,FaaS) 简单来讲就是功能运行在独立容器中,基于事件驱动,由第三方托管功能。

后端即服务(Backend as a Service,Baas) 使用第三方服务来达到目的。

升级(维护)模式

蓝绿部署


蓝绿部署(Blue-green deployment),可以称之为 可不停机维护,主要分为蓝(Blue)和绿(Green)两个版本,其流程如下:

  1. 当更新时,将所有访问流量分给绿色部分来提供服务,从而升级蓝色集群。
  2. 当蓝色集群升级完后,来将绿色流量切换到蓝色集群,从而升级绿色集群
  3. 升级完成后将访问流量分配到全部集群中

滚动部署


滚动部署(Rolling deployment),指每次只一个或多个服务暂停。进行更新,后将此服务继续投入使用,直到集群中所有服务都被更新或完成为止。

金丝雀发布(灰度发布)

为什么叫金丝雀发布?
在以前矿工开矿的时候,需要在下矿洞前需要检查下方是否有毒气,矿工会放一只金丝雀进去,来去探路是否有毒气。

金丝雀发布也叫灰度发布(Canary release / Grayscale release),其主要是一部分更新,一部分使用老版本,当新版本用户没什么建议的话,则全部将迁移到新版本中。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布

Spring cloud 业务拆分与管理模式

Domain Driver Design(DDD)


在微服务设计中,我们通常会存在一种不知道如何进行划分的问题,到底是根据功能、组织结构进行划分还是其他方式。

而为了解决这种问题,领域驱动设计(Domain Driver Design)提供了一种划分方法论,说白了就是可以解决微服务划分时遇到的问题。

首先,DDD 主要结构为四层,分别为 基础设施层 (Infrastructure)、领域层 (Domain)、应用层 (Application)、用户界面层 (Interfaces)(也被称为表示层、展现层、接口层)

领域

首先,我们需要通过 DDD 来指导我们的划分方法,而不是让他主导我们,从而让我们更加困惑。因此我们需要知道一件事情,就是 分清问题和方案,问题是需要解决的,而方案是用来解决问题的。

当谈论领域的时候(含子域、通用子域)实际上都是在讨论问题域,而问题域就是 我们需要解决什么问题

而到 限界上下文、聚合、实体、仓库 这些实际上是在 谈论解决方案 来解决领域时提出的问题。

在搜索领域中,我们可以从其主页面中来看出遇到的一些非常典型的问题:

  1. 如何保持知名度
  2. 如何让搜素变得更精准
  3. 如何找到一个合适的度进行营销

而这些问题的解决问题的方案可以是:

  1. 与浏览器之间进行绑定
  2. 加大数据爬取和关键词的定位
  3. 与搜索结果中布局一致

设计

在设计中,主要解决的就是让领域专家和技术专家深入合作,并建立一个软件问题提模型,而这个模型主要可以解决:

  1. 可以表达领域概念和业务流程
  2. 每个领域都有自己的解决方案

这就是设计在 DDD 主要的作用,DDD 是一种基于领域为中心来驱动的软件的开发思想,可以通过此来帮助我们解决复杂的业务问题,因此需要对每个 领域提出问题来对应一个领域模型,来解决问题

模型

在 DDD 中,主要将模型划分为四层,分别为基础层(Infrastructure)、领域层(Domain)、应用层(Application)、用户界面层(Interfaces),这些被简称为 IDAI。

通俗来讲模型就是一个解决方案空间,是一个为了准确定义需要问题而构造的一个抽象模型。而模型主要可以解决类似与业务领域问题,以及分析满足系统的功能性要求即 IDAI。

专家

通过领域提出的问题我们可以得知,这些问题的提出基本上不是开发人员非常擅长的,所以此时需要 领域专家 来提出问题,由 技术专家 解决问题,这也是 DDD 成功实施的主要条件。

事务管理模式

事物管理模式(ACID,Atomicity Consistency Isolation Durability)要比 DDD 更好理解,他在上一章就以一小部分的形式出现了,当时我们所介绍的是原子性(Atomicity)。

一个逻辑工作单元需要成为事物,需要满足 ACID属性,分别为:

  1. A:原子性(Atomicity),事物操作中,要么全做,要么都不做
  2. C:一致性(Consistency),事务执行完后,从同样状态,转向另一个同样的状态(可以理解为要么是全部执行中,要么是全部暂停中)
  3. I :隔离性(Isolation),事务执行时不被打扰
  4. D:持久性(Durability),事务提交后,对数据的改变就是永久性的。

2/3PC

2/3PC 算法模式(Two / Three Phase Commit,二/三阶段提交)主要是为了让分布式系统在事务处理时 可以实现 ACID属性 所设计的算法。

2PC

首先我们可以将他看成两个阶段,一个是 欲提交,而另一个则是 提交 阶段,其中:

欲提交阶段


欲提交阶段从名字就可以看出他并不是真正的提交阶段,而是处于向资源管理器询问是否可以提交,这其中主要分为三个阶段:

  1. 向所有资源管理器发送询问
  2. 资源管理器执行事物操作,如资源上锁等
  3. 返回信息给事物管理器(成功情况下)
执行事务提交阶段


执行事务提交阶段可以理解为欲提交阶段后的一个过程,他的主要有四个流程,分别为:

  1. 请求提交:事务管理器向资源管理器发出提交请求
  2. 事务提交:资源管理器收到请求后,开始事务提交操作,完成后释放事务资源
  3. 反馈结果:完成事务提交之后,向事务管理器发送 已提交(ACK) 消息
  4. 完成事务:事务管理器收到了所有资源管理器反馈的 ACK 后即完成提交。

假如其中某个资源管理器返回了未提交,或者说等待超时,那么将会中断事务,其流程如下:

  1. 发送事务请求:事务管理器向资源管理器发送回滚请求
  2. 事务回滚:资源管理器通过撤销信息来执行事务回滚,释放事务资源
  3. 反馈事务回滚结果:完成事务回滚后发送 ACK (协调者发布)消息
  4. 中断事务:当事务管理器收到了来自所有参与者反馈的 ACK 消息后中断服务。
缺点
堵塞


尽管 2PC 看似非常的优秀,但是如果认真品一下就会发现,这么多资源管理器,如果只有中某个资源管理器在执行事物提交阶段的时候,返回了 未提交,之后的所有提交是不是都需要进行等待而无法继续完成操作?

单点

单点即一旦事务管理器出现了问题,那么整个 2PC 的提交将无法运转。或者说一旦在提交的时候出现了问题,那么所有的资源管理器都会处于锁定状态,从而无法继续提供服务。

3PC

3PC 和 2PC 不同之处在于他有三个阶段,分别为 欲提交(PreCommit)、可以提交(CanCommit)、提交(doCommit)。在3PC 中三个阶段都有一个核心的理念,即 询问时不会锁定资源管理器,除非所有人 Ack 了才开始锁,因此他解决了 2PC 的堵塞问题。

而单点问题 3PC 则通过假设资源管理器无法即时接收到来自事务管理器的信息,他们会默认执行 doCommit ,而不会一直卡在那里不动,最终导致无法提供服务。

TTY Try-Confirm-Cancel


TTY 主要有三个接口,分别为 尝试(Try)、确认(Confirm)、取消(Cancel)。会比 2/3PC 更加好理解,因此他三个阶段功能如下:

  1. Try:检查所有业务的 一致性、并预留业务资源保证在事务执行时不会被其他事务打扰
  2. Confirm:使用 Try 阶段预留的业务资源来执行业务
  3. Cancel:取消执行业务,并释放 Try 阶段预留资源

消息中间件模式


通过引入消息中间件,可以进行异步处理、系统解耦、事务管理。从而解决多个系统之间同步通信所造成的阻塞,并将这些耦合在一起。消息中间件模式与 TCC 同样是最终一致性来管理事务,而 2/3PC 则是强一致。

在这个过程中,主要有三个要素,分别是将数据消息投递到中间件服务器中,通过消息确认机制保证成功投递。然后通过消费者可以正常使用消息,最后来保证第一个事务先执行。

目前世面上主流的消息中间件有 Redis、ActiveMQ、RabbitMQ、RocketMQ、Kafka等。

Saga 模式


Saga是根据 Hector 和 Kenneth 与 1987 年所发表的论文 《sagas》作为理论基础而出现的补偿协议。

Saga 模式其主要的就是 将长事务拆分为多个且可交错运行的子事务合集 而这些子事务都保持一定的一致性,都将由 saga 事务协调器来进行协调。

如果每一个子事务都正常结束,则整个事务完成。假设有某个子事务失败,那么则整个事务失败,并根据相反顺序来执行补偿步骤。

需要注意的是,Saga 由一系列的 T 事务组成,每个 T都对应一个补偿动作 C,用于撤销 T 所造成的结果。

恢复策略

向后恢复

向后恢复(backward recovery),补偿所有已完成事务。如果某一个子任务失败,则之前所有成功的子事物即 整个saga执行结果撤销

向前恢复

向前恢复(forward recovery),将失败的事务重试,假设所有失败的业务最终都会成功,在这种情况下就没有补偿事务什么事情了,常见与必须成功的场景。‘

Paxos 一致性算法


Paxos 算法由 Lamport 所提出的一种基于消息传递的分布式一致性算法,也被认为是类似算法中最为有效的,因此使其获得了 2013年图灵奖,他的主要作用就是 在分布式系统中就某个值,来达成一致

角色

首先在 Paxos 算法中最终要的就是角色,非常重要,主要有三个,分别为:

  1. 提议者(Proposer)
  2. 决策者(Acceptor )
  3. 学习者(Learner)

决策

首先,提议者(Proposer) 提出议案(Proposal),信息中包含了案件编号(Proposer id)以及提议的值(Value)

之后 决策者(Acceptor) 参与决策,回应 提议者(Proposer) 提案,如果收到的提案(Proposal)获得多数 提议者(Acceptor) 的接受,则该提案被批准

最后 学习者(Learner) 来学习从会议中获得到达成一致的提案(value)。

2F+1 我们可以理解为,假设要是决策者 5个 同意 5个拒绝,那么就不会构成会议的决策。在这种情况下,如果有2倍的 决策者 人数并加一,就可以保证信息的正确,使得会议正常进行。

Paxos 算法允许运行在宕机故障的异步系统内,也不要求可靠的消息传递,他使用了多数机制保证了 2F(faulty)+1 的容错,即 2F(faulty)+1 个节点最多允许 F 个节点出现问题。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布